/*
 * File    : WebPlugin.java
 * Created : 23-Jan-2004
 * By      : parg
 * 
 * Azureus - a Java Bittorrent client
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details ( see the LICENSE file ).
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.gudy.azureus2.ui.webplugin;

/**
 * @author parg
 *
 */

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.zip.GZIPOutputStream;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.util.AENetworkClassifier;
import org.gudy.azureus2.core3.util.AERunnable;
import org.gudy.azureus2.core3.util.AESemaphore;
import org.gudy.azureus2.core3.util.AsyncDispatcher;
import org.gudy.azureus2.core3.util.Base32;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.RandomUtils;
import org.gudy.azureus2.core3.util.SHA1Hasher;
import org.gudy.azureus2.core3.util.SystemProperties;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimeFormatter;
import org.gudy.azureus2.core3.util.UrlUtils;
import org.gudy.azureus2.plugins.Plugin;
import org.gudy.azureus2.plugins.PluginConfig;
import org.gudy.azureus2.plugins.PluginException;
import org.gudy.azureus2.plugins.PluginInterface;
import org.gudy.azureus2.plugins.PluginListener;
import org.gudy.azureus2.plugins.ipfilter.IPRange;
import org.gudy.azureus2.plugins.logging.LoggerChannel;
import org.gudy.azureus2.plugins.logging.LoggerChannelListener;
import org.gudy.azureus2.plugins.tracker.Tracker;
import org.gudy.azureus2.plugins.tracker.TrackerException;
import org.gudy.azureus2.plugins.tracker.TrackerTorrent;
import org.gudy.azureus2.plugins.tracker.web.TrackerAuthenticationAdapter;
import org.gudy.azureus2.plugins.tracker.web.TrackerWebContext;
import org.gudy.azureus2.plugins.tracker.web.TrackerWebPageGenerator;
import org.gudy.azureus2.plugins.tracker.web.TrackerWebPageRequest;
import org.gudy.azureus2.plugins.tracker.web.TrackerWebPageResponse;
import org.gudy.azureus2.plugins.ui.UIManager;
import org.gudy.azureus2.plugins.ui.config.BooleanParameter;
import org.gudy.azureus2.plugins.ui.config.ConfigSection;
import org.gudy.azureus2.plugins.ui.config.HyperlinkParameter;
import org.gudy.azureus2.plugins.ui.config.InfoParameter;
import org.gudy.azureus2.plugins.ui.config.IntParameter;
import org.gudy.azureus2.plugins.ui.config.LabelParameter;
import org.gudy.azureus2.plugins.ui.config.Parameter;
import org.gudy.azureus2.plugins.ui.config.ParameterListener;
import org.gudy.azureus2.plugins.ui.config.PasswordParameter;
import org.gudy.azureus2.plugins.ui.config.StringListParameter;
import org.gudy.azureus2.plugins.ui.config.StringParameter;
import org.gudy.azureus2.plugins.ui.model.BasicPluginConfigModel;
import org.gudy.azureus2.plugins.ui.model.BasicPluginViewModel;
import org.json.simple.JSONObject;

import com.aelitis.azureus.core.pairing.PairedService;
import com.aelitis.azureus.core.pairing.PairedServiceRequestHandler;
import com.aelitis.azureus.core.pairing.PairingConnectionData;
import com.aelitis.azureus.core.pairing.PairingManager;
import com.aelitis.azureus.core.pairing.PairingManagerFactory;
import com.aelitis.azureus.core.pairing.PairingManagerListener;
import com.aelitis.azureus.core.proxy.AEProxyFactory;
import com.aelitis.azureus.plugins.upnp.UPnPMapping;
import com.aelitis.azureus.plugins.upnp.UPnPPlugin;
import com.aelitis.azureus.util.JSONUtils;

public class WebPlugin implements Plugin, TrackerWebPageGenerator {
    public static final String PR_ENABLE = "Enable"; // Boolean
    public static final String PR_DISABLABLE = "Disablable"; // Boolean
    public static final String PR_PORT = "Port"; // Integer
    public static final String PR_BIND_IP = "Bind IP"; // String
    public static final String PR_ROOT_RESOURCE = "Root Resource"; // String
    public static final String PR_ROOT_DIR = "Root Dir"; // String
    public static final String PR_ACCESS = "Access"; // String
    public static final String PR_LOG = "DefaultLoggerChannel"; // LoggerChannel
    public static final String PR_CONFIG_MODEL_PARAMS = "DefaultConfigModelParams"; // String[] params to use when creating config model
    public static final String PR_CONFIG_MODEL = "DefaultConfigModel"; // BasicPluginConfigModel
    public static final String PR_VIEW_MODEL = "DefaultViewModel"; // BasicPluginViewModel
    public static final String PR_HIDE_RESOURCE_CONFIG = "DefaultHideResourceConfig"; // Boolean
    public static final String PR_ENABLE_KEEP_ALIVE = "DefaultEnableKeepAlive"; // Boolean
    public static final String PR_PAIRING_SID = "PairingSID"; // String

    public static final String PROPERTIES_MIGRATED = "Properties Migrated";
    public static final String CONFIG_MIGRATED = "Config Migrated";
    public static final String PAIRING_MIGRATED = "Pairing Migrated";
    public static final String PAIRING_SESSION_KEY = "Pairing Session Key";

    public static final String CONFIG_PASSWORD_ENABLE = "Password Enable";
    public static final boolean CONFIG_PASSWORD_ENABLE_DEFAULT = false;

    public static final String CONFIG_PAIRING_ENABLE = "Pairing Enable";
    public static final boolean CONFIG_PAIRING_ENABLE_DEFAULT = true;

    public static final String CONFIG_PORT_OVERRIDE = "Port Override";

    public static final String CONFIG_PAIRING_AUTO_AUTH = "Pairing Auto Auth";
    public static final boolean CONFIG_PAIRING_AUTO_AUTH_DEFAULT = true;

    public static final String CONFIG_ENABLE = PR_ENABLE;
    public boolean CONFIG_ENABLE_DEFAULT = true;

    public static final String CONFIG_USER = "User";
    public static final String CONFIG_USER_DEFAULT = "";

    public static final String CONFIG_PASSWORD = "Password";
    public static final byte[] CONFIG_PASSWORD_DEFAULT = {};

    public static final String CONFIG_PORT = PR_PORT;
    public int CONFIG_PORT_DEFAULT = 8089;

    public static final String CONFIG_BIND_IP = PR_BIND_IP;
    public String CONFIG_BIND_IP_DEFAULT = "";

    public static final String CONFIG_PROTOCOL = "Protocol";
    public static final String CONFIG_PROTOCOL_DEFAULT = "HTTP";

    public static final String CONFIG_UPNP_ENABLE = "UPnP Enable";
    public static final boolean CONFIG_UPNP_ENABLE_DEFAULT = true;

    public static final String CONFIG_HOME_PAGE = "Home Page";
    public static final String CONFIG_HOME_PAGE_DEFAULT = "index.html";

    public static final String CONFIG_ROOT_DIR = PR_ROOT_DIR;
    public String CONFIG_ROOT_DIR_DEFAULT = "";

    public static final String CONFIG_ROOT_RESOURCE = PR_ROOT_RESOURCE;
    public String CONFIG_ROOT_RESOURCE_DEFAULT = "";

    public static final String CONFIG_MODE = "Mode";
    public static final String CONFIG_MODE_FULL = "full";
    public static final String CONFIG_MODE_DEFAULT = CONFIG_MODE_FULL;

    public static final String CONFIG_ACCESS = PR_ACCESS;
    public String CONFIG_ACCESS_DEFAULT = "all";

    protected static final String NL = "\r\n";

    protected static final String[] welcome_pages = { "index.html", "index.htm", "index.php", "index.tmpl" };
    protected static File[] welcome_files;

    private static final AsyncDispatcher network_dispatcher = new AsyncDispatcher("webplugin:netdispatch", 5000);

    protected PluginInterface plugin_interface; // unfortunately this is accessed by webui - fix sometime

    private LoggerChannel log;
    private PluginConfig plugin_config;
    private BasicPluginViewModel view_model;
    private BasicPluginConfigModel config_model;

    private String p_sid;

    private StringParameter param_home;
    private StringParameter param_rootdir;
    private StringParameter param_rootres;

    private IntParameter param_port;
    private StringListParameter param_protocol;
    private StringParameter param_bind;

    private StringParameter param_access;

    private InfoParameter param_i2p_dest;

    private BooleanParameter p_upnp_enable;

    private BooleanParameter pw_enable;
    private StringParameter p_user_name;
    private PasswordParameter p_password;

    private BooleanParameter param_auto_auth;
    private IntParameter param_port_or;
    private boolean setting_auto_auth;
    private String pairing_access_code;
    private String pairing_session_code;

    private boolean plugin_enabled;

    private String home_page;
    private String file_root;
    private String resource_root;

    private String root_dir;

    private boolean ip_range_all = false;
    private List<IPRange> ip_ranges;

    private TrackerWebContext tracker_context;
    private UPnPMapping upnp_mapping;
    private PairingManagerListener pairing_listener;

    private Properties properties;

    private static ThreadLocal<String> tls = new ThreadLocal<String>() {
        public String initialValue() {
            return (null);
        }
    };

    private static final int LOGOUT_GRACE_MILLIS = 5 * 1000;
    private static final String GRACE_PERIOD_MARKER = "<grace_period>";

    private Map<String, Long> logout_timer = new HashMap<String, Long>();

    public WebPlugin() {
        properties = new Properties();
    }

    public WebPlugin(Properties defaults) {
        properties = defaults;
    }

    public void initialize(PluginInterface _plugin_interface)

    throws PluginException {
        plugin_interface = _plugin_interface;

        plugin_config = plugin_interface.getPluginconfig();

        Properties plugin_properties = plugin_interface.getPluginProperties();

        if (plugin_properties != null) {

            Object o = plugin_properties.get("plugin." + PR_ROOT_DIR.replaceAll(" ", "_"));

            if (o instanceof String) {

                properties.put(PR_ROOT_DIR, o);
            }
        }

        Boolean pr_enable = (Boolean) properties.get(PR_ENABLE);

        if (pr_enable != null) {

            CONFIG_ENABLE_DEFAULT = pr_enable.booleanValue();
        }

        Integer pr_port = (Integer) properties.get(PR_PORT);

        if (pr_port != null) {

            CONFIG_PORT_DEFAULT = pr_port.intValue();
        }

        String pr_bind_ip = (String) properties.get(PR_BIND_IP);

        if (pr_bind_ip != null) {

            CONFIG_BIND_IP_DEFAULT = pr_bind_ip.trim();
        }

        String pr_root_resource = (String) properties.get(PR_ROOT_RESOURCE);

        if (pr_root_resource != null) {

            CONFIG_ROOT_RESOURCE_DEFAULT = pr_root_resource;
        }

        String pr_root_dir = (String) properties.get(PR_ROOT_DIR);

        if (pr_root_dir != null) {

            CONFIG_ROOT_DIR_DEFAULT = pr_root_dir;
        }

        String pr_access = (String) properties.get(PR_ACCESS);

        if (pr_access != null) {

            CONFIG_ACCESS_DEFAULT = pr_access;
        }

        Boolean pr_hide_resource_config = (Boolean) properties.get(PR_HIDE_RESOURCE_CONFIG);

        log = (LoggerChannel) properties.get(PR_LOG);

        if (log == null) {

            log = plugin_interface.getLogger().getChannel("WebPlugin");
        }

        p_sid = (String) properties.get(PR_PAIRING_SID);

        UIManager ui_manager = plugin_interface.getUIManager();

        view_model = (BasicPluginViewModel) properties.get(PR_VIEW_MODEL);

        if (view_model == null) {

            view_model = ui_manager.createBasicPluginViewModel(plugin_interface.getPluginName());
        }

        String plugin_id = plugin_interface.getPluginID();

        String sConfigSectionID = "plugins." + plugin_id;

        view_model.setConfigSectionID(sConfigSectionID);
        view_model.getStatus().setText("Running");
        view_model.getActivity().setVisible(false);
        view_model.getProgress().setVisible(false);

        log.addListener(new LoggerChannelListener() {
            public void messageLogged(int type, String message) {
                view_model.getLogArea().appendText(message + "\n");
            }

            public void messageLogged(String str, Throwable error) {
                view_model.getLogArea().appendText(str + "\n");
                view_model.getLogArea().appendText(error.toString() + "\n");
            }
        });

        config_model = (BasicPluginConfigModel) properties.get(PR_CONFIG_MODEL);

        if (config_model == null) {

            String[] cm_params = (String[]) properties.get(PR_CONFIG_MODEL_PARAMS);

            if (cm_params == null || cm_params.length == 0) {

                config_model = ui_manager.createBasicPluginConfigModel(ConfigSection.SECTION_PLUGINS, sConfigSectionID);

            } else if (cm_params.length == 1) {

                config_model = ui_manager.createBasicPluginConfigModel(cm_params[0]);

            } else {

                config_model = ui_manager.createBasicPluginConfigModel(cm_params[0], cm_params[1]);
            }
        }

        boolean save_needed = false;

        if (!plugin_config.getPluginBooleanParameter(CONFIG_MIGRATED, false)) {

            plugin_config.setPluginParameter(CONFIG_MIGRATED, true);

            save_needed = true;

            plugin_config.setPluginParameter(CONFIG_PASSWORD_ENABLE, plugin_config.getBooleanParameter("Tracker Password Enable Web",
                    CONFIG_PASSWORD_ENABLE_DEFAULT));

            plugin_config.setPluginParameter(CONFIG_USER, plugin_config.getStringParameter("Tracker Username", CONFIG_USER_DEFAULT));

            plugin_config.setPluginParameter(CONFIG_PASSWORD, plugin_config.getByteParameter("Tracker Password", CONFIG_PASSWORD_DEFAULT));

        }

        if (!plugin_config.getPluginBooleanParameter(PROPERTIES_MIGRATED, false)) {

            plugin_config.setPluginParameter(PROPERTIES_MIGRATED, true);

            Properties props = plugin_interface.getPluginProperties();

            // make sure we've got an old properties file too

            if (props.getProperty("port", "").length() > 0) {

                save_needed = true;

                String prop_port = props.getProperty("port", "" + CONFIG_PORT_DEFAULT);
                String prop_protocol = props.getProperty("protocol", CONFIG_PROTOCOL_DEFAULT);
                String prop_home = props.getProperty("homepage", CONFIG_HOME_PAGE_DEFAULT);
                String prop_rootdir = props.getProperty("rootdir", CONFIG_ROOT_DIR_DEFAULT);
                String prop_rootres = props.getProperty("rootresource", CONFIG_ROOT_RESOURCE_DEFAULT);
                String prop_mode = props.getProperty("mode", CONFIG_MODE_DEFAULT);
                String prop_access = props.getProperty("access", CONFIG_ACCESS_DEFAULT);

                int prop_port_int = CONFIG_PORT_DEFAULT;

                try {
                    prop_port_int = Integer.parseInt(prop_port);

                } catch (Throwable e) {
                }

                plugin_config.setPluginParameter(CONFIG_PORT, prop_port_int);
                plugin_config.setPluginParameter(CONFIG_PROTOCOL, prop_protocol);
                plugin_config.setPluginParameter(CONFIG_HOME_PAGE, prop_home);
                plugin_config.setPluginParameter(CONFIG_ROOT_DIR, prop_rootdir);
                plugin_config.setPluginParameter(CONFIG_ROOT_RESOURCE, prop_rootres);
                plugin_config.setPluginParameter(CONFIG_MODE, prop_mode);
                plugin_config.setPluginParameter(CONFIG_ACCESS, prop_access);

                File props_file = new File(plugin_interface.getPluginDirectoryName(), "plugin.properties");

                PrintWriter pw = null;

                try {
                    File backup = new File(plugin_interface.getPluginDirectoryName(), "plugin.properties.bak");

                    props_file.renameTo(backup);

                    pw = new PrintWriter(new FileWriter(props_file));

                    pw.println("plugin.class=" + props.getProperty("plugin.class"));
                    pw.println("plugin.name=" + props.getProperty("plugin.name"));
                    pw.println("plugin.version=" + props.getProperty("plugin.version"));
                    pw.println("plugin.id=" + props.getProperty("plugin.id"));
                    pw.println("");
                    pw.println("# configuration has been migrated to plugin config - see view->config->plugins");
                    pw.println("# in the SWT user interface");

                    log.logAlert(LoggerChannel.LT_INFORMATION, plugin_interface.getPluginName()
                            + " - plugin.properties settings migrated to plugin configuration.");

                } catch (Throwable e) {

                    Debug.printStackTrace(e);

                    log.logAlert(LoggerChannel.LT_ERROR, plugin_interface.getPluginName() + " - plugin.properties settings migration failed.");

                } finally {

                    if (pw != null) {

                        pw.close();
                    }
                }
            }
        }

        if (save_needed) {

            plugin_config.save();
        }

        Boolean disablable = (Boolean) properties.get(PR_DISABLABLE);

        final BooleanParameter param_enable;

        if (disablable != null && disablable) {

            param_enable = config_model.addBooleanParameter2(CONFIG_ENABLE, "webui.enable", CONFIG_ENABLE_DEFAULT);

            plugin_enabled = param_enable.getValue();

        } else {
            param_enable = null;

            plugin_enabled = true;
        }

        initStage(1);

        // connection group

        param_port = config_model.addIntParameter2(CONFIG_PORT, "webui.port", CONFIG_PORT_DEFAULT);

        param_bind = config_model.addStringParameter2(CONFIG_BIND_IP, "webui.bindip", CONFIG_BIND_IP_DEFAULT);

        param_protocol =
                config_model.addStringListParameter2(CONFIG_PROTOCOL, "webui.protocol", new String[] { "http", "https" }, CONFIG_PROTOCOL_DEFAULT);

        ParameterListener update_server_listener = new ParameterListener() {
            public void parameterChanged(Parameter param) {
                setupServer();
            }
        };

        param_port.addListener(update_server_listener);
        param_bind.addListener(update_server_listener);
        param_protocol.addListener(update_server_listener);

        param_i2p_dest = config_model.addInfoParameter2("webui.i2p_dest", "");

        param_i2p_dest.setVisible(false);

        if (param_enable != null) {
            COConfigurationManager.registerExportedParameter(plugin_id + ".enable", param_enable.getConfigKeyName());
        }
        COConfigurationManager.registerExportedParameter(plugin_id + ".port", param_port.getConfigKeyName());
        COConfigurationManager.registerExportedParameter(plugin_id + ".protocol", param_protocol.getConfigKeyName());

        p_upnp_enable = config_model.addBooleanParameter2(CONFIG_UPNP_ENABLE, "webui.upnpenable", CONFIG_UPNP_ENABLE_DEFAULT);

        p_upnp_enable.addListener(new ParameterListener() {
            public void parameterChanged(Parameter param) {
                setupUPnP();
            }
        });

        plugin_interface.addListener(new PluginListener() {
            public void initializationComplete() {
                setupUPnP();
            }

            public void closedownInitiated() {
            }

            public void closedownComplete() {
            }
        });

        final LabelParameter pairing_info;
        final BooleanParameter pairing_enable;
        final HyperlinkParameter pairing_test;
        final HyperlinkParameter connection_test;

        if (p_sid != null) {

            final PairingManager pm = PairingManagerFactory.getSingleton();

            pairing_info = config_model.addLabelParameter2("webui.pairing.info." + (pm.isEnabled() ? "y" : "n"));

            pairing_enable = config_model.addBooleanParameter2(CONFIG_PAIRING_ENABLE, "webui.pairingenable", CONFIG_PAIRING_ENABLE_DEFAULT);

            if (!plugin_config.getPluginBooleanParameter(PAIRING_MIGRATED, false)) {

                // if they already have a password, don't override it by setting auto-auth

                boolean has_pw_enabled = plugin_config.getPluginBooleanParameter(CONFIG_PASSWORD_ENABLE, CONFIG_PASSWORD_ENABLE_DEFAULT);

                if (has_pw_enabled) {

                    plugin_config.setPluginParameter(CONFIG_PAIRING_AUTO_AUTH, false);
                }

                plugin_config.setPluginParameter(PAIRING_MIGRATED, true);
            }

            param_port_or = config_model.addIntParameter2(CONFIG_PORT_OVERRIDE, "webui.port.override", 0);

            param_auto_auth =
                    config_model.addBooleanParameter2(CONFIG_PAIRING_AUTO_AUTH, "webui.pairing.autoauth", CONFIG_PAIRING_AUTO_AUTH_DEFAULT);

            param_auto_auth.addListener(new ParameterListener() {
                public void parameterChanged(Parameter param) {
                    if (pairing_enable.getValue() && pm.isEnabled()) {

                        setupAutoAuth();

                    } else {

                        setupSessionCode(null);
                    }
                }
            });

            connection_test = config_model.addHyperlinkParameter2("webui.connectiontest", getConnectionTestURL(p_sid));

            pairing_test = config_model.addHyperlinkParameter2("webui.pairingtest", "http://remote.vuze.com/?sid=" + p_sid);

            // listeners setup later as they depend on userame params etc

            String sid_key = "Plugin." + plugin_id + ".pairing.sid";

            COConfigurationManager.setStringDefault(sid_key, p_sid);

            COConfigurationManager.registerExportedParameter(plugin_id + ".pairing.sid", sid_key);
            COConfigurationManager.registerExportedParameter(plugin_id + ".pairing.enable", pairing_enable.getConfigKeyName());
            COConfigurationManager.registerExportedParameter(plugin_id + ".pairing.auto_auth", param_auto_auth.getConfigKeyName());

        } else {
            pairing_info = null;
            pairing_enable = null;
            param_auto_auth = null;
            param_port_or = null;
            pairing_test = null;
            connection_test = null;
        }

        config_model.createGroup("ConfigView.section.Pairing", new Parameter[] { pairing_info, pairing_enable, param_port_or, param_auto_auth,
                connection_test, pairing_test, });

        config_model.createGroup("ConfigView.section.server", new Parameter[] { param_port, param_bind, param_protocol, param_i2p_dest,
                p_upnp_enable, });

        param_home = config_model.addStringParameter2(CONFIG_HOME_PAGE, "webui.homepage", CONFIG_HOME_PAGE_DEFAULT);
        param_rootdir = config_model.addStringParameter2(CONFIG_ROOT_DIR, "webui.rootdir", CONFIG_ROOT_DIR_DEFAULT);
        param_rootres = config_model.addStringParameter2(CONFIG_ROOT_RESOURCE, "webui.rootres", CONFIG_ROOT_RESOURCE_DEFAULT);

        if (pr_hide_resource_config != null && pr_hide_resource_config.booleanValue()) {

            param_home.setVisible(false);
            param_rootdir.setVisible(false);
            param_rootres.setVisible(false);

        } else {

            ParameterListener update_resources_listener = new ParameterListener() {
                public void parameterChanged(Parameter param) {
                    setupResources();
                }
            };

            param_home.addListener(update_resources_listener);
            param_rootdir.addListener(update_resources_listener);
            param_rootres.addListener(update_resources_listener);
        }

        // access group

        LabelParameter a_label1 = config_model.addLabelParameter2("webui.mode.info");
        StringListParameter param_mode =
                config_model.addStringListParameter2(CONFIG_MODE, "webui.mode", new String[] { "full", "view" }, CONFIG_MODE_DEFAULT);

        LabelParameter a_label2 = config_model.addLabelParameter2("webui.access.info");

        param_access = config_model.addStringParameter2(CONFIG_ACCESS, "webui.access", CONFIG_ACCESS_DEFAULT);

        param_access.addListener(new ParameterListener() {
            public void parameterChanged(Parameter param) {
                setupAccess();
            }
        });

        pw_enable = config_model.addBooleanParameter2(CONFIG_PASSWORD_ENABLE, "webui.passwordenable", CONFIG_PASSWORD_ENABLE_DEFAULT);

        p_user_name = config_model.addStringParameter2(CONFIG_USER, "webui.user", CONFIG_USER_DEFAULT);

        p_password = config_model.addPasswordParameter2(CONFIG_PASSWORD, "webui.password", PasswordParameter.ET_SHA1, CONFIG_PASSWORD_DEFAULT);

        pw_enable.addEnabledOnSelection(p_user_name);
        pw_enable.addEnabledOnSelection(p_password);

        ParameterListener auth_change_listener = new ParameterListener() {
            public void parameterChanged(Parameter param) {
                if (param_auto_auth != null) {

                    if (!setting_auto_auth) {

                        log("Disabling pairing auto-authentication as overridden by user");

                        param_auto_auth.setValue(false);
                    }
                }

                if (param == p_user_name || param == p_password) {

                    setupSessionCode(null);
                }
            }
        };

        p_user_name.addListener(auth_change_listener);
        p_password.addListener(auth_change_listener);
        pw_enable.addListener(auth_change_listener);

        config_model.createGroup("webui.group.access", new Parameter[] { a_label1, param_mode, a_label2, param_access, pw_enable, p_user_name,
                p_password, });

        if (p_sid != null) {

            final PairingManager pm = PairingManagerFactory.getSingleton();

            pairing_enable.addListener(new ParameterListener() {
                public void parameterChanged(Parameter param) {
                    boolean enabled = pairing_enable.getValue();

                    param_auto_auth.setEnabled(pm.isEnabled() && enabled);
                    param_port_or.setEnabled(pm.isEnabled() && enabled);

                    boolean test_ok = pm.isEnabled() && pairing_enable.getValue() && pm.peekAccessCode() != null && !pm.hasActionOutstanding();

                    pairing_test.setEnabled(test_ok);
                    connection_test.setEnabled(test_ok);

                    setupPairing(p_sid, enabled);
                }
            });

            pairing_listener = new PairingManagerListener() {
                public void somethingChanged(PairingManager pm) {
                    pairing_info.setLabelKey("webui.pairing.info." + (pm.isEnabled() ? "y" : "n"));

                    if (plugin_enabled) {

                        pairing_enable.setEnabled(pm.isEnabled());

                        param_auto_auth.setEnabled(pm.isEnabled() && pairing_enable.getValue());
                        param_port_or.setEnabled(pm.isEnabled() && pairing_enable.getValue());

                        boolean test_ok = pm.isEnabled() && pairing_enable.getValue() && pm.peekAccessCode() != null && !pm.hasActionOutstanding();

                        pairing_test.setEnabled(test_ok);
                        connection_test.setEnabled(test_ok);
                    }

                    connection_test.setHyperlink(getConnectionTestURL(p_sid));

                    setupPairing(p_sid, pairing_enable.getValue());
                }
            };

            pairing_listener.somethingChanged(pm);

            pm.addListener(pairing_listener);

            setupPairing(p_sid, pairing_enable.getValue());

            ParameterListener update_pairing_listener = new ParameterListener() {
                public void parameterChanged(Parameter param) {
                    updatePairing(p_sid);

                    setupUPnP();
                }
            };

            param_port.addListener(update_pairing_listener);

            param_port_or.addListener(update_pairing_listener);

            param_protocol.addListener(update_pairing_listener);

            /*
             * config_model.addActionParameter2( "test", "test" ).addListener( new ParameterListener() { public void parameterChanged( Parameter param
             * ) { try{ pm.testService( p_sid, new PairingTestListener() { public void testStarted( PairingTest test ) { System.out.println(
             * "Test starts" ); } public void testComplete( PairingTest test) { System.out.println( "Test complete: " + test.getOutcome() + "/" +
             * test.getErrorMessage()); } }); }catch( Throwable e ){ Debug.out( e ); } } });
             */
        }

        if (param_enable != null) {

            final List<Parameter> changed_params = new ArrayList<Parameter>();

            if (!plugin_enabled) {

                Parameter[] params = config_model.getParameters();

                for (Parameter param : params) {

                    if (param == param_enable) {

                        continue;
                    }

                    if (param.isEnabled()) {

                        changed_params.add(param);

                        param.setEnabled(false);
                    }
                }
            }

            param_enable.addListener(new ParameterListener() {
                public void parameterChanged(Parameter e_p) {
                    // this doesn't quite work as tne enabler/disabler parameter logic is implemented
                    // badly and only toggles the UI component, not the enabled state of the
                    // underlying parameter. grr. better than nothing though

                    plugin_enabled = ((BooleanParameter) e_p).getValue();

                    if (plugin_enabled) {

                        for (Parameter p : changed_params) {

                            p.setEnabled(true);
                        }
                    } else {

                        changed_params.clear();

                        Parameter[] params = config_model.getParameters();

                        for (Parameter param : params) {

                            if (param == e_p) {

                                continue;
                            }

                            if (param.isEnabled()) {

                                changed_params.add(param);

                                param.setEnabled(false);
                            }
                        }
                    }

                    setupServer();

                    setupUPnP();

                    if (p_sid != null) {

                        setupPairing(p_sid, pairing_enable.getValue());
                    }
                }
            });
        }

        // end config

        setupResources();

        setupAccess();

        setupServer();
    }

    protected void initStage(int num) {
    }

    private String getConnectionTestURL(String sid) {
        String res = "http://pair.vuze.com/pairing/web/test?sid=" + sid;

        PairingManager pm = PairingManagerFactory.getSingleton();

        if (pm.isEnabled()) {

            String ac = pm.peekAccessCode();

            if (ac != null) {

                res += "&ac=" + ac;
            }
        }

        return (res);
    }

    protected boolean isPluginEnabled() {
        return (plugin_enabled);
    }

    protected void unloadPlugin() {
        if (view_model != null) {

            view_model.destroy();

            view_model = null;
        }

        if (config_model != null) {

            config_model.destroy();

            config_model = null;
        }

        if (tracker_context != null) {

            tracker_context.destroy();

            tracker_context = null;
        }

        if (upnp_mapping != null) {

            upnp_mapping.destroy();

            upnp_mapping = null;
        }

        if (pairing_listener != null) {

            PairingManager pm = PairingManagerFactory.getSingleton();

            pm.removeListener(pairing_listener);

            pairing_listener = null;
        }
    }

    private void setupResources() {
        home_page = param_home.getValue().trim();

        if (home_page.length() == 0) {

            home_page = null;

        } else if (!home_page.startsWith("/")) {

            home_page = "/" + home_page;
        }

        resource_root = param_rootres.getValue().trim();

        if (resource_root.length() == 0) {

            resource_root = null;

        } else if (resource_root.startsWith("/")) {

            resource_root = resource_root.substring(1);
        }

        root_dir = param_rootdir.getValue().trim();

        if (root_dir.length() == 0) {

            file_root = plugin_interface.getPluginDirectoryName();

            if (file_root == null) {

                file_root = SystemProperties.getUserPath() + "web";
            }
        } else {

            // absolute or relative

            if (root_dir.startsWith(File.separator) || root_dir.indexOf(":") != -1) {

                file_root = root_dir;

            } else {

                if (File.separatorChar != '/' && root_dir.contains("/")) {

                    root_dir = root_dir.replace('/', File.separatorChar);
                }

                // try relative to plugin dir

                file_root = plugin_interface.getPluginDirectoryName();

                if (file_root != null) {

                    file_root = file_root + File.separator + root_dir;

                    if (!new File(file_root).exists()) {

                        file_root = null;
                    }
                }

                if (file_root == null) {

                    file_root = SystemProperties.getUserPath() + "web" + File.separator + root_dir;
                }
            }
        }

        File f_root = new File(file_root);

        if (!f_root.exists()) {

            String error = "WebPlugin: root dir '" + file_root + "' doesn't exist";

            log.log(LoggerChannel.LT_ERROR, error);

        } else if (!f_root.isDirectory()) {

            String error = "WebPlugin: root dir '" + file_root + "' isn't a directory";

            log.log(LoggerChannel.LT_ERROR, error);
        }

        welcome_files = new File[welcome_pages.length];

        for (int i = 0; i < welcome_pages.length; i++) {

            welcome_files[i] = new File(file_root + File.separator + welcome_pages[i]);
        }
    }

    private void setupAccess() {
        String access_str = param_access.getValue().trim();

        String ip_ranges_str = "";

        ip_ranges = null;
        ip_range_all = false;

        if (access_str.length() > 7 && Character.isDigit(access_str.charAt(0))) {

            String[] ranges = access_str.replace(';', ',').split(",");

            ip_ranges = new ArrayList<IPRange>();

            for (String range : ranges) {

                range = range.trim();

                if (range.length() > 7) {

                    IPRange ip_range = plugin_interface.getIPFilter().createRange(true);

                    int sep = range.indexOf("-");

                    if (sep == -1) {

                        ip_range.setStartIP(range);

                        ip_range.setEndIP(range);

                    } else {

                        ip_range.setStartIP(range.substring(0, sep).trim());

                        ip_range.setEndIP(range.substring(sep + 1).trim());
                    }

                    ip_range.checkValid();

                    if (!ip_range.isValid()) {

                        log.log(LoggerChannel.LT_ERROR, "Access parameter '" + range + "' is invalid");

                    } else {

                        ip_ranges.add(ip_range);

                        ip_ranges_str += (ip_ranges_str.length() == 0 ? "" : ", ") + ip_range.getStartIP() + " - " + ip_range.getEndIP();
                    }
                }
            }

            if (ip_ranges.size() == 0) {

                ip_ranges = null;
            }
        } else {

            if (access_str.equalsIgnoreCase("all") || access_str.length() == 0) {

                ip_range_all = true;
            }
        }

        log.log(LoggerChannel.LT_INFORMATION, "Acceptable IP range = " + (ip_ranges == null ? (ip_range_all ? "all" : "local") : (ip_ranges_str)));
    }

    protected void setupServer() {
        try {
            if (!plugin_enabled) {

                if (tracker_context != null) {

                    tracker_context.destroy();

                    tracker_context = null;
                }

                return;
            }

            final int port = param_port.getValue();

            String protocol_str = param_protocol.getValue().trim();

            String bind_ip_str = param_bind.getValue().trim();

            InetAddress bind_ip = null;

            if (bind_ip_str.length() > 0) {

                try {
                    bind_ip = InetAddress.getByName(bind_ip_str);

                } catch (Throwable e) {

                    log.log(LoggerChannel.LT_ERROR, "Bind IP parameter '" + bind_ip_str + "' is invalid");

                }
            }

            if (tracker_context != null) {

                URL url = tracker_context.getURLs()[0];

                String existing_protocol = url.getProtocol();
                int existing_port = url.getPort() == -1 ? url.getDefaultPort() : url.getPort();
                InetAddress existing_bind_ip = tracker_context.getBindIP();

                if (existing_port == port && existing_protocol.equalsIgnoreCase(protocol_str) && sameAddress(bind_ip, existing_bind_ip)) {

                    return;
                }

                tracker_context.destroy();

                tracker_context = null;
            }

            int protocol = protocol_str.equalsIgnoreCase("HTTP") ? Tracker.PR_HTTP : Tracker.PR_HTTPS;

            log.log(LoggerChannel.LT_INFORMATION, "Server initialisation: port = " + port
                    + (bind_ip == null ? "" : (", bind = " + bind_ip_str + ")")) + ", protocol = " + protocol_str
                    + (root_dir.length() == 0 ? "" : (", root = " + root_dir)));

            tracker_context =
                    plugin_interface.getTracker().createWebContext(Constants.APP_NAME + " - " + plugin_interface.getPluginName(), port, protocol,
                            bind_ip);

            network_dispatcher.dispatch(new AERunnable() {
                public void runSupport() {
                    Map<String, Object> options = new HashMap<String, Object>();

                    options.put(AEProxyFactory.SP_PORT, port);

                    Map<String, Object> reply =
                            AEProxyFactory.getPluginServerProxy(plugin_interface.getPluginName(), AENetworkClassifier.AT_I2P, plugin_interface
                                    .getPluginID(), options);

                    if (reply != null) {

                        param_i2p_dest.setVisible(true);

                        String host = (String) reply.get("host");

                        if (!param_i2p_dest.getValue().equals(host)) {

                            param_i2p_dest.setValue(host);

                            if (p_sid != null) {

                                updatePairing(p_sid);
                            }
                        }
                    }
                }
            });

            Boolean pr_enable_keep_alive = (Boolean) properties.get(PR_ENABLE_KEEP_ALIVE);

            if (pr_enable_keep_alive != null && pr_enable_keep_alive) {

                tracker_context.setEnableKeepAlive(true);
            }

            tracker_context.addPageGenerator(this);

            tracker_context.addAuthenticationListener(new TrackerAuthenticationAdapter() {
                private String last_pw = "";
                private byte[] last_hash = {};

                private final int DELAY = 10 * 1000;

                private Map<String, Object[]> fail_map = new HashMap<String, Object[]>();

                public boolean authenticate(String headers, URL resource, String user, String pw) {
                    // System.out.println( resource + ": " + user + "/" + pw );

                    long now = SystemTime.getMonotonousTime();

                    String client_address = getHeaderField(headers, "X-Real-IP");

                    if (client_address == null) {

                        client_address = "<unknown>";
                    }

                    synchronized (logout_timer) {

                        Long logout_time = logout_timer.get(client_address);

                        if (logout_time != null && now - logout_time <= LOGOUT_GRACE_MILLIS) {

                            tls.set(GRACE_PERIOD_MARKER);

                            return (true);
                        }
                    }

                    boolean result = authenticateSupport(headers, resource, user, pw);

                    if (!result) {

                        // don't delay clients that keep failing to send auth entirely (old Android browsers for example)

                        if (!pw.equals("")) {

                            AESemaphore waiter = null;

                            synchronized (fail_map) {

                                Object[] x = fail_map.get(client_address);

                                if (x == null) {

                                    x = new Object[] { new AESemaphore("af:waiter"), new Long(-1), new Long(-1), now };

                                    fail_map.put(client_address, x);

                                } else {

                                    x[1] = x[2];
                                    x[2] = x[3];
                                    x[3] = now;

                                    long t = (Long) x[1];

                                    if (now - t < 10 * 1000) {

                                        log("Too many recent authentication failures from '" + client_address + "' - rate limiting");

                                        x[2] = now + DELAY;
                                        // there's a bug where flipping the password on doesn't reset the pw so we automatically fail without checking
                                        // this is not the correct fix, but it works
                                        last_pw = "";
                                        waiter = (AESemaphore) x[0];
                                    }
                                }
                            }

                            if (waiter != null) {

                                waiter.reserve(DELAY);
                            }
                        }
                    } else {
                        // Some clients have no cookie support and will always try with
                        // no auth info, then, once getting a failed response, try again
                        // with the auth info.
                        // This results in a loop of 1 good, 1 bad.
                        // Prevent this from causing the "too many recent failures" delay to kick in by removing from map
                        // on goodness

                        synchronized (fail_map) {

                            fail_map.remove(client_address);
                        }

                        String cookies = getHeaderField(headers, "Cookie");

                        if (pairing_session_code != null) {

                            if (cookies == null || !cookies.contains(pairing_session_code)) {

                                tls.set(pairing_session_code);
                            }
                        }
                    }

                    recordAuthRequest(client_address, result);

                    if (!result) {

                        // going to be generous here as (old android browsers at least) sometimes fail to provide
                        // auth on .png files

                        // no I'm not, too many risks associated with this (e.g. xmwebui has some
                        // prefix url logic which may be exploitable)

                        // if ( resource.getPath().endsWith( ".png" )){
                        //
                        // result = true;
                        // }
                    }

                    return (result);
                }

                private boolean authenticateSupport(String headers, URL resource, String user, String pw) {
                    boolean result;

                    boolean auto_auth = param_auto_auth != null && param_auto_auth.getValue();

                    if (!pw_enable.getValue()) {

                        result = true;

                    } else {

                        if (auto_auth) {

                            user = user.trim().toLowerCase();

                            pw = pw.toUpperCase();
                        }

                        if (!user.equals(p_user_name.getValue())) {

                            result = false;

                        } else {

                            byte[] hash = last_hash;

                            if (!last_pw.equals(pw)) {

                                hash =
                                        plugin_interface.getUtilities().getSecurityManager().calculateSHA1(
                                                auto_auth ? pw.toUpperCase().getBytes() : pw.getBytes());

                                last_pw = pw;
                                last_hash = hash;
                            }

                            result = Arrays.equals(hash, p_password.getValue());
                        }
                    }

                    if (result) {

                        // user name and password match, see if we've come from the pairing process

                        checkCookieSet(headers, resource);

                    } else if (auto_auth) {

                        // either the ac is in the url, referer or we have a cookie set

                        int x = checkCookieSet(headers, resource);

                        if (x == 1) {

                            result = true;

                        } else if (x == 0) {

                            result = hasOurCookie(getHeaderField(headers, "Cookie"));
                        }
                    } else {

                        result = hasOurCookie(getHeaderField(headers, "Cookie"));
                    }

                    return (result);
                }

                /**
                 * 
                 * @param headers
                 * @param resource
                 * @return 0 = unknown, 1 = ok, 2 = bad
                 */

                private int checkCookieSet(String headers, URL resource) {
                    if (pairing_access_code == null) {

                        return (2);
                    }

                    String[] locations = { resource.getQuery(), getHeaderField(headers, "Referer") };

                    for (String location : locations) {

                        if (location != null) {

                            boolean skip_fail = false;
                            int param_len = 0;

                            int p1 = location.indexOf("vuze_pairing_ac=");

                            if (p1 == -1) {

                                p1 = location.indexOf("ac=");

                                if (p1 != -1) {

                                    param_len = 3;

                                    skip_fail = true;
                                }
                            } else {

                                param_len = 16;
                            }

                            if (p1 != -1) {

                                int p2 = location.indexOf('&', p1);

                                String ac = location.substring(p1 + param_len, p2 == -1 ? location.length() : p2).trim();

                                p2 = ac.indexOf('#');

                                if (p2 != -1) {

                                    ac = ac.substring(0, p2);
                                }

                                if (ac.equalsIgnoreCase(pairing_access_code)) {

                                    tls.set(pairing_session_code);

                                    return (1);

                                } else {

                                    if (!skip_fail) {

                                        return (2);
                                    }
                                }
                            }
                        }
                    }

                    return (0);
                }

                private String getHeaderField(String headers, String field) {
                    String lc_headers = headers.toLowerCase();

                    int p1 = lc_headers.indexOf(field.toLowerCase() + ":");

                    if (p1 != -1) {

                        int p2 = lc_headers.indexOf('\n', p1);

                        if (p2 != -1) {

                            return (headers.substring(p1 + field.length() + 1, p2).trim());
                        }
                    }

                    return (null);
                }
            });

        } catch (TrackerException e) {

            log.log("Server initialisation failed", e);
        }
    }

    private boolean hasOurCookie(String cookies) {
        if (cookies == null) {

            return (false);
        }

        String[] cookie_list = cookies.split(";");

        for (String cookie : cookie_list) {

            String[] bits = cookie.split("=");

            if (bits.length == 2) {

                if (bits[0].trim().equals("vuze_pairing_sc")) {

                    if (bits[1].trim().equals(pairing_session_code)) {

                        return (true);
                    }
                }
            }
        }

        return (false);
    }

    private boolean sameAddress(InetAddress a1, InetAddress a2) {
        if (a1 == null && a2 == null) {

            return (true);

        } else if (a1 == null || a2 == null) {

            return (false);

        } else {

            return (a1.equals(a2));
        }
    }

    protected void setupUPnP() {
        if (!plugin_enabled || !p_upnp_enable.getValue()) {

            if (upnp_mapping != null) {

                log("Removing UPnP mapping");

                upnp_mapping.destroy();

                upnp_mapping = null;
            }

            return;
        }

        PluginInterface pi_upnp = plugin_interface.getPluginManager().getPluginInterfaceByClass(UPnPPlugin.class);

        if (pi_upnp == null) {

            log.log("No UPnP plugin available, not attempting port mapping");

        } else {

            int port = param_port.getValue();

            if (upnp_mapping != null) {

                if (upnp_mapping.getPort() == port) {

                    return;
                }

                log("Updating UPnP mapping");

                upnp_mapping.destroy();

            } else {

                log("Creating UPnP mapping");
            }

            upnp_mapping = ((UPnPPlugin) pi_upnp.getPlugin()).addMapping(plugin_interface.getPluginName(), true, port, true);
        }
    }

    protected void setupPairing(String sid, boolean pairing_enabled) {
        PairingManager pm = PairingManagerFactory.getSingleton();

        PairedService service = pm.getService(sid);

        if (plugin_enabled && pairing_enabled && pm.isEnabled()) {

            setupAutoAuth();

            if (service == null) {

                log("Adding pairing service");

                service = pm.addService(sid, new PairedServiceRequestHandler() {
                    public byte[] handleRequest(InetAddress originator, String endpoint_url, byte[] request)

                    throws IOException {
                        return (handleTunnelRequest(originator, endpoint_url, request));
                    }
                });

                PairingConnectionData cd = service.getConnectionData();

                try {
                    updatePairing(cd);

                } finally {

                    cd.sync();
                }
            }
        } else {

            pairing_access_code = null;

            setupSessionCode(null);

            if (service != null) {

                log("Removing pairing service");

                service.remove();
            }
        }
    }

    private void setupSessionCode(String key) {
        if (key == null) {

            key = Base32.encode(p_user_name.getValue().getBytes()) + Base32.encode(p_password.getValue());
        }

        synchronized (this) {

            String existing_key = plugin_config.getPluginStringParameter(PAIRING_SESSION_KEY, "");

            String[] bits = existing_key.split("=");

            if (bits.length == 2 && bits[0].equals(key)) {

                pairing_session_code = bits[1];

            } else {

                pairing_session_code = Base32.encode(RandomUtils.nextSecureHash());

                plugin_config.setPluginParameter(PAIRING_SESSION_KEY, key + "=" + pairing_session_code);
            }
        }
    }

    protected void setupAutoAuth() {
        PairingManager pm = PairingManagerFactory.getSingleton();

        String ac = pm.peekAccessCode();

        pairing_access_code = ac;

        // good time to check the default pairing auth settings

        if (pairing_access_code != null && param_auto_auth.getValue()) {

            setupSessionCode(ac);

            try {
                setting_auto_auth = true;

                if (!p_user_name.getValue().equals("vuze")) {

                    p_user_name.setValue("vuze");
                }

                SHA1Hasher hasher = new SHA1Hasher();

                byte[] encoded = hasher.calculateHash(pairing_access_code.getBytes());

                if (!Arrays.equals(p_password.getValue(), encoded)) {

                    p_password.setValue(pairing_access_code);
                }

                if (!pw_enable.getValue()) {

                    pw_enable.setValue(true);
                }
            } finally {

                setting_auto_auth = false;
            }
        } else {

            setupSessionCode(null);
        }
    }

    protected void updatePairing(String sid) {
        PairingManager pm = PairingManagerFactory.getSingleton();

        PairedService service = pm.getService(sid);

        if (service != null) {

            PairingConnectionData cd = service.getConnectionData();

            log("Updating pairing information");

            try {
                updatePairing(cd);

            } finally {

                cd.sync();
            }
        }
    }

    protected void updatePairing(PairingConnectionData cd) {
        cd.setAttribute(PairingConnectionData.ATTR_PORT, String.valueOf(param_port.getValue()));

        int override = param_port_or == null ? 0 : param_port_or.getValue();

        if (override > 0) {

            cd.setAttribute(PairingConnectionData.ATTR_PORT_OVERRIDE, String.valueOf(override));

        } else {

            cd.setAttribute(PairingConnectionData.ATTR_PORT_OVERRIDE, null);
        }

        cd.setAttribute(PairingConnectionData.ATTR_PROTOCOL, param_protocol.getValue());

        if (param_i2p_dest.isVisible()) {

            String host = param_i2p_dest.getValue();

            if (host.length() > 0) {

                cd.setAttribute(PairingConnectionData.ATTR_I2P, host);
            }
        }
    }

    public int getPort() {
        return (param_port.getValue());
    }

    public String getProtocol() {
        return (param_protocol.getValue());
    }

    public void setUserAndPassword(String user, String password) {
        p_user_name.setValue(user);
        p_password.setValue(password);
        pw_enable.setValue(true);
    }

    public void unsetUserAndPassword() {
        pw_enable.setValue(false);
    }

    private void recordAuthRequest(String client_ip, boolean good) {
        PairingManager pm = PairingManagerFactory.getSingleton();

        pm.recordRequest(plugin_interface.getPluginName(), client_ip, good);
    }

    private void recordRequest(TrackerWebPageRequest request, boolean good, boolean is_tunnel) {
        PairingManager pm = PairingManagerFactory.getSingleton();

        String str = request.getClientAddress();

        if (is_tunnel) {

            str = "Tunnel (" + str + ")";
        }

        pm.recordRequest(plugin_interface.getPluginName(), str, good);
    }

    public boolean generateSupport(TrackerWebPageRequest request, TrackerWebPageResponse response)

    throws IOException {
        return (false);
    }

    private byte[] handleTunnelRequest(final InetAddress originator, String endpoint_url, final byte[] request_bytes)

    throws IOException {
        int q_pos = endpoint_url.indexOf('?');

        boolean raw = true;

        if (q_pos != -1) {

            String params = endpoint_url.substring(q_pos + 1);

            String[] args = params.split("&");

            String new_endpoint = endpoint_url.substring(0, q_pos);

            String sep = "?";

            for (String arg : args) {

                if (arg.startsWith("tunnel_format=")) {

                    String temp = arg.substring(14);

                    if (temp.startsWith("h")) {

                        raw = false;
                    }
                } else {

                    new_endpoint += sep + arg;

                    sep = "&";
                }
            }

            endpoint_url = new_endpoint;
        }

        final String f_endpoint_url = endpoint_url;
        final JSONObject request_headers = new JSONObject();

        final int data_start;

        if (raw) {

            data_start = 0;

        } else {
            int request_header_len = ((request_bytes[0] << 8) & 0x0000ff00) | (request_bytes[1] & 0x000000ff);

            String reply_json_str = new String(request_bytes, 2, request_header_len, "UTF-8");

            request_headers.putAll(JSONUtils.decodeJSON(reply_json_str));

            data_start = request_header_len + 2;
        }

        TrackerWebPageRequest request = new TrackerWebPageRequest() {
            public Tracker getTracker() {
                return (null);
            }

            public String getClientAddress() {
                return (originator.getHostAddress());
            }

            public InetSocketAddress getClientAddress2() {
                return (new InetSocketAddress(originator, 0));
            }

            public InetSocketAddress getLocalAddress() {
                return (new InetSocketAddress("127.0.0.1", 0));
            }

            public String getUser() {
                return (null);
            }

            public String getURL() {
                String url = (String) request_headers.get("HTTP-URL");

                if (url != null) {

                    return (url);
                }

                return (f_endpoint_url);
            }

            public String getHeader() {
                return ("");
            }

            public Map getHeaders() {
                return (request_headers);
            }

            public InputStream getInputStream() {
                return (new ByteArrayInputStream(request_bytes, data_start, request_bytes.length - data_start));
            }

            public URL getAbsoluteURL() {
                try {
                    return (new URL("http://127.0.0.1" + getURL()));

                } catch (Throwable e) {

                    return (null);
                }
            }

            public TrackerWebContext getContext() {
                return (null);
            }
        };
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();

        final Map reply_headers = new HashMap();

        TrackerWebPageResponse response = new TrackerWebPageResponse() {
            public OutputStream getOutputStream() {
                return (baos);
            }

            public void setReplyStatus(int status) {
                reply_headers.put("HTTP-Status", String.valueOf(status));
            }

            public void setContentType(String type) {
                reply_headers.put("Content-Type", type);
            }

            public void setLastModified(long time) {
            }

            public void setExpires(long time) {
            }

            public void setHeader(String name, String value) {
                reply_headers.put(name, value);
            }

            public void setGZIP(boolean gzip) {
            }

            public boolean useFile(String root_dir, String relative_url)

            throws IOException {
                Debug.out("Not supported");

                return (false);
            }

            public void useStream(String file_type, InputStream stream)

            throws IOException {
                Debug.out("Not supported");
            }

            public void writeTorrent(TrackerTorrent torrent)

            throws IOException {
                Debug.out("Not supported");
            }

            public void setAsynchronous(boolean async)

            throws IOException {
                Debug.out("Not supported");
            }

            public boolean getAsynchronous() {
                return (false);
            }

            public OutputStream getRawOutputStream()

            throws IOException {
                Debug.out("Not supported");

                throw (new IOException("Not supported"));
            }
        };

        try {
            byte[] bytes;

            if (generate2(request, response, true)) {

                bytes = baos.toByteArray();

            } else {

                Debug.out("Tunnelled request not handled: " + request.getURL());

                response.setReplyStatus(404);

                bytes = new byte[0];
            }

            if (raw) {

                return (bytes);

            } else {

                String accept_encoding = (String) request_headers.get("Accept-Encoding");

                if (accept_encoding != null && accept_encoding.contains("gzip")) {

                    reply_headers.put("Content-Encoding", "gzip");

                    ByteArrayOutputStream temp = new ByteArrayOutputStream(bytes.length + 512);

                    GZIPOutputStream gos = new GZIPOutputStream(temp);

                    gos.write(bytes);

                    gos.close();

                    bytes = temp.toByteArray();
                }

                ByteArrayOutputStream baos2 = new ByteArrayOutputStream(bytes.length + 512);

                String header_json = JSONUtils.encodeToJSON(reply_headers);

                byte[] header_bytes = header_json.getBytes("UTF-8");

                int header_len = header_bytes.length;

                byte[] header_len_bytes = new byte[] { (byte) (header_len >> 8), (byte) header_len };

                baos2.write(header_len_bytes);
                baos2.write(header_bytes);
                baos2.write(bytes);

                return (baos2.toByteArray());
            }
        } catch (Throwable e) {

            Debug.out(e);

            return (new byte[0]);
        }
    }

    public boolean generate(TrackerWebPageRequest request, TrackerWebPageResponse response)

    throws IOException {
        String url = request.getURL();

        if (url.startsWith("/pairing/tunnel/")) {

            long error_code = 1;

            try {
                final PairingManager pm = PairingManagerFactory.getSingleton();

                if (pm.isEnabled()) {

                    if (pm.isSRPEnabled()) {

                        return (pm.handleLocalTunnel(request, response));

                    } else {

                        error_code = 5;

                        throw (new IOException("Secure pairing is not enabled"));
                    }
                } else {

                    error_code = 5;

                    throw (new IOException("Pairing is not enabled"));
                }
            } catch (Throwable e) {

                JSONObject json = new JSONObject();

                JSONObject error = new JSONObject();

                json.put("error", error);

                error.put("msg", Debug.getNestedExceptionMessage(e));
                error.put("code", error_code);

                return (returnJSON(response, JSONUtils.encodeToJSON(json)));
            }
        }

        return (generate2(request, response, false));
    }

    private boolean generate2(TrackerWebPageRequest request, TrackerWebPageResponse response, boolean is_tunnel)

    throws IOException {
        // System.out.println( request.getURL());

        String client = request.getClientAddress();

        if (!ip_range_all) {

            // System.out.println( "client = " + client );

            try {
                boolean valid_ip = true;
                InetAddress ia = InetAddress.getByName(client);

                if (ip_ranges == null) {

                    if (!ia.isLoopbackAddress()) {

                        log.log(LoggerChannel.LT_ERROR, "Client '" + client + "' is not local, rejecting");

                        valid_ip = false;
                    }
                } else {

                    boolean ok = false;

                    for (IPRange range : ip_ranges) {

                        if (range.isInRange(ia.getHostAddress())) {

                            ok = true;
                        }
                    }

                    if (!ok) {

                        log.log(LoggerChannel.LT_ERROR, "Client '" + client + "' (" + ia.getHostAddress() + ") is not in range, rejecting");

                        valid_ip = false;
                    }
                }

                if (!valid_ip) {

                    response.setReplyStatus(403);

                    recordRequest(request, false, is_tunnel);

                    return (returnTextPlain(response, "Cannot access resource from this IP address."));
                }

            } catch (Throwable e) {

                Debug.printStackTrace(e);

                recordRequest(request, false, is_tunnel);

                return (false);
            }
        }

        recordRequest(request, true, is_tunnel);

        String url = request.getURL();

        if (url.toString().endsWith(".class")) {

            System.out.println("WebPlugin::generate:" + url);
        }

        String cookie_to_set = tls.get();

        if (cookie_to_set == GRACE_PERIOD_MARKER) {

            return (returnTextPlain(response, "Logout in progress, please try again later."));
        }

        if (cookie_to_set != null) {

            // set session cookie

            response.setHeader("Set-Cookie", "vuze_pairing_sc=" + cookie_to_set + "; path=/; HttpOnly");

            tls.set(null);
        }

        URL full_url = request.getAbsoluteURL();

        String full_url_path = full_url.getPath();

        if (full_url_path.equals("/isPairedServiceAvailable")) {

            String redirect = getArgumentFromURL(full_url, "redirect_to");

            if (redirect != null) {

                try {
                    URL target = new URL(redirect);

                    String host = target.getHost();

                    if (!Constants.isAzureusDomain(host)) {

                        if (!InetAddress.getByName(host).isLoopbackAddress()) {

                            log("Invalid redirect host: " + host);

                            redirect = null;
                        }
                    }
                } catch (Throwable e) {

                    Debug.out(e);

                    redirect = null;
                }
            }

            if (redirect != null) {

                response.setReplyStatus(302);

                response.setHeader("Location", redirect);

                return (true);
            }

            String callback = getArgumentFromURL(full_url, "jsoncallback");

            if (callback != null) {

                return (returnTextPlain(response, callback + "( {'pairedserviceavailable':true} )"));
            }
        } else if (full_url_path.equals("/isServicePaired")) {

            boolean paired = cookie_to_set != null || hasOurCookie((String) request.getHeaders().get("cookie"));

            // DON'T use returnJSON here as it DOESN'T work in the web ui for some reason!

            return (returnTextPlain(response, "{ 'servicepaired': " + (paired ? "true" : "false") + " }"));

        } else if (full_url_path.equals("/pairedServiceLogout")) {

            synchronized (logout_timer) {

                logout_timer.put(client, SystemTime.getMonotonousTime());
            }

            response.setHeader("Set-Cookie", "vuze_pairing_sc=<deleted>, expires=" + TimeFormatter.getCookieDate(0));

            String redirect = getArgumentFromURL(full_url, "redirect_to");

            if (redirect != null) {

                try {
                    URL target = new URL(redirect);

                    String host = target.getHost();

                    if (!Constants.isAzureusDomain(host)) {

                        if (!InetAddress.getByName(host).isLoopbackAddress()) {

                            log("Invalid redirect host: " + host);

                            redirect = null;
                        }
                    }
                } catch (Throwable e) {

                    Debug.out(e);

                    redirect = null;
                }
            }
            if (redirect == null) {

                return (returnTextPlain(response, ""));

            } else {

                response.setReplyStatus(302);

                response.setHeader("Location", redirect);

                return (true);
            }
        }

        request.getHeaders().put("x-vuze-is-tunnel", is_tunnel ? "true" : "false");

        if (generateSupport(request, response)) {

            return (true);
        }

        if (is_tunnel) {

            return (false);
        }

        if (url.equals("/") || url.startsWith("/?")) {

            url = "/";

            if (home_page != null) {

                url = home_page;

            } else {

                for (int i = 0; i < welcome_files.length; i++) {

                    if (welcome_files[i].exists()) {

                        url = "/" + welcome_pages[i];

                        break;
                    }
                }
            }
        }

        // first try file system for data

        if (response.useFile(file_root, url)) {

            return (true);
        }

        // now try jars

        String resource_name = url;

        if (resource_name.startsWith("/")) {

            resource_name = resource_name.substring(1);
        }

        int pos = resource_name.lastIndexOf(".");

        if (pos != -1) {

            String type = resource_name.substring(pos + 1);

            ClassLoader cl = plugin_interface.getPluginClassLoader();

            InputStream is = cl.getResourceAsStream(resource_name);

            if (is == null) {

                // failed absolute load, try relative

                if (resource_root != null) {

                    resource_name = resource_root + "/" + resource_name;

                    is = cl.getResourceAsStream(resource_name);
                }
            }

            // System.out.println( resource_name + "->" + is + ", url = " + url );

            if (is != null) {

                try {
                    response.useStream(type, is);

                } finally {

                    is.close();
                }

                return (true);
            }
        }

        return (false);
    }

    private String getArgumentFromURL(URL url, String argument) {
        String query = url.getQuery();

        if (query != null) {

            String[] args = query.split("&");

            for (String arg : args) {

                String[] x = arg.split("=");

                if (x.length == 2) {

                    if (x[0].equals(argument)) {

                        return (UrlUtils.decode(x[1]));
                    }
                }
            }
        }

        return (null);
    }

    private boolean returnTextPlain(TrackerWebPageResponse response, String str) {
        return (returnStuff(response, "text/plain", str));
    }

    private boolean returnJSON(TrackerWebPageResponse response, String str)

    throws IOException {
        response.setContentType("application/json; charset=UTF-8");

        OutputStream os = response.getOutputStream();

        os.write(str.getBytes("UTF-8"));

        return (true);
    }

    private boolean returnStuff(TrackerWebPageResponse response, String content_type, String str) {
        response.setContentType(content_type);

        PrintWriter pw = new PrintWriter(response.getOutputStream());

        pw.println(str);

        pw.flush();

        pw.close();

        return (true);
    }

    protected BasicPluginConfigModel getConfigModel() {
        return (config_model);
    }

    protected BasicPluginViewModel getViewModel() {
        return this.view_model;
    }

    protected void log(String str) {
        log.log(str);
    }

    protected void log(String str, Throwable e) {
        log.log(str, e);
    }
}
